﻿/*
   Copyright 2012 University of Southampton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;

namespace uk.ac.soton.ses.DAL
{
    public partial class ExperimentDataFile
    {
        /// <summary>
        /// Make a copy of the data file object so it can be used elsewhere
        /// </summary>
        /// <returns>The data file copy</returns>
        public ExperimentDataFile Copy()
        {
            ExperimentDataFile sourceDataFile = this;
            using (MemoryStream ms = new MemoryStream())
            {
                IFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(ms, sourceDataFile);
                ms.Seek(0, SeekOrigin.Begin);
                return (ExperimentDataFile)binaryFormatter.Deserialize(ms);
            }
        }

        /// <summary>
        /// Returns the full path to the data file, combining base path and file name
        /// </summary>
        /// <returns>The full path to the data file</returns>
        public string GetFullPath()
        {
            return Path.Combine(this.BasePath, this.Filename);
        }

        /// <summary>
        /// Returns a URL to the data file, using the configuration options to calculate the URL
        /// </summary>
        /// <returns>A URL to the data file</returns>
        public string GetWebPath(MaterialsMetadataContext dbcontext)
        {
            try
            {
                //Load the file system watcher experiment associated with this experiment data file so we can lookup the web path
                HDCConfigurationOptionsFileSystemWatcher config = this.GetConfigurationOptionsFileSystemWatcher(dbcontext);
                string localDownloadPath = this.GetFullPath();
                string webDownloadPath = localDownloadPath.Replace(config.LocalPath, config.WebPath).Replace('\\', '/');
                return webDownloadPath;
            }
            catch (ConfigurationNotFoundException)
            {
                return null;
            }
        }

        /// <summary>
        /// Returns a UNC path for the data file, using the configuration options of the file system watcher that found the file.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the operation.</param>
        /// <returns>The UNC path to the file as a string.</returns>
        public string GetUNCPath(MaterialsMetadataContext dbcontext)
        {
            try
            {
                //Load the file system watcher experiment associated with this experiment data file so we can lookup the web path
                HDCConfigurationOptionsFileSystemWatcher config = this.GetConfigurationOptionsFileSystemWatcher(dbcontext);
                string localDownloadPath = this.GetFullPath();
                string downloadPath = localDownloadPath.Replace(config.LocalPath, config.UNCPath);
                return downloadPath;
            }
            catch (ConfigurationNotFoundException)
            {
                return null;
            }
        }

        /// <summary>
        /// Looks up the configuration options for the file system watcher that discovered the file.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the operation.</param>
        /// <returns>The configuration options class of the watcher that found the file.</returns>
        public HDCConfigurationOptionsFileSystemWatcher GetConfigurationOptionsFileSystemWatcher(MaterialsMetadataContext dbcontext)
        {
            //Load the file system watcher experiment associated with this experiment data file so we can lookup the web path
            Experiment optionsExperiment = dbcontext.Experiments.FirstOrDefault(exp => exp.ID == this.FileStoreOptionsExperimentID);
            if (optionsExperiment != null)
            {
                HDCConfigurationOptionsFileSystemWatcher config = new HDCConfigurationOptionsFileSystemWatcher() { ConfigurationContainerID = optionsExperiment.ID };
                config.LoadConfigurationFromDatabase(dbcontext);
                return config;
            }
            else
            {
                throw new ConfigurationNotFoundException("Data file does not have an associated file system watcher");
            }
        }

        /// <summary>
        /// Using data in the database, this suggests some related datafiles
        /// </summary>
        /// <param name="count">How many data files to suggest</param>
        /// <param name="findDeleted">Whether to include deleted files</param>
        /// <returns>A list of related data files</returns>
        public List<ExperimentDataFile> GetRelatedItems(int count = 5, bool findDeleted = false)
        {
            HDCConfigurationOptions configOptions = HDCConfiguration.GetConfigurationOptions();
            using (MaterialsMetadataContext dbcontext = new MaterialsMetadataContext(configOptions.DatabaseOptions.EntityConnectionString))
            {
                IQueryable<ExperimentDataFile> relatedItems =
                    dbcontext.ExperimentDataFiles
                    .Where(df => df.ExperimentDataFileType.ID == this.ExperimentDataFileType.ID);
                if (!findDeleted)
                    relatedItems = relatedItems.Where(df => !df.IsDeleted);
                return relatedItems.Take(count).ToList();
            }
        }

        /// <summary>
        /// Sets the image file as a dataset's thumbnail. This will only succeed if the file has a valid image extension.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the operation.</param>
        /// <returns>True if successful, otherwise false.</returns>
        public bool SetAsThumbnail(MaterialsMetadataContext dbcontext)
        {
            List<string> imageExtensions = new List<string>(){".jpg",".jpeg",".png",".gif",".bmp"};
            string extension = Path.GetExtension(this.Filename);
            if (imageExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
            {
                Experiment thisExperiment = dbcontext.GetExperimentByID(this.Experiment.ID);
                thisExperiment.SetAsThumbnail(this.GetWebPath(dbcontext) ?? String.Empty, dbcontext);
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Parses the data file as XTEKCT format, which is basically key=value grouped by [sections in brackets], and imports as metadata entries
        /// </summary>
        /// <param name="dbcontext">A database context to perform the operation.</param>
        /// <param name="saveChanges">Whether to update the database context or wait for SaveChanges to be called manually.</param>
        /// <returns>An integer representing how many metadata entries were created.</returns>
        public int ImportAsXTEKCTFile(MaterialsMetadataContext dbcontext, bool saveChanges)
        {
            Dictionary<string, string> typeMapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            { 
                { "xrayua", "float" }, 
                { "xraykv", "float" }
            };

            int count = 0;
            int experimentID = this.Experiment.ID;

            //Parse the file
            FileParser_XTEKCT filedata = new FileParser_XTEKCT(this.GetFullPath());

            //The properties will be created as follows:
            // Experiment
            // |-File name
            // |--Top-level properties
            // |--Sections
            // |---Section properties

            //Create a parent parameter to put file's parameters under
            ExperimentParameter fileParentParam = new ExperimentParameter() { Name = this.Filename };
            dbcontext.AddParameterToExperiment(experimentID, fileParentParam);

            //Create the top-level properties
            foreach (KeyValuePair<string, string> pair in filedata.Properties)
            {
                //Create parameter
                ExperimentParameter newParam = new ExperimentParameter() { Name = pair.Key, Value = pair.Value };
                dbcontext.AddParameterToExperiment(experimentID, newParam);
                count++;
                //Nest the new parameter under the file's parameters
                fileParentParam.AddChildParameter(dbcontext, newParam);

            }

            //Create the sections
            foreach (XTEKCTFileSection section in filedata.Sections)
            {
                //Create a parameter for the section 
                ExperimentParameter newSectionParam = new ExperimentParameter() { Name = section.SectionName };
                dbcontext.AddParameterToExperiment(experimentID, newSectionParam);
                count++;
                //Nest the new parameter under the file's parameters
                fileParentParam.AddChildParameter(dbcontext, newSectionParam);

                foreach (KeyValuePair<string, string> pair in section.Properties)
                {
                    //Create a parameter for the property
                    ExperimentParameter childParam = new ExperimentParameter() { Name = pair.Key, Value = pair.Value };

                    //Add a parameter type if it is found in the type mapping dictionary
                    if (!String.IsNullOrEmpty(pair.Key) && typeMapping.ContainsKey(pair.Key))
                    {
                        childParam.Type = typeMapping[pair.Key];
                    }

                    //Add the parameter to the experiment
                    dbcontext.AddParameterToExperiment(experimentID, childParam);
                    count++;

                    //Nest the new parameter under the section parameter
                    newSectionParam.AddChildParameter(dbcontext, childParam);
                }
            }

            if (saveChanges)
            {
                dbcontext.SaveChanges();
            }

            return count;
        }

        /// <summary>
        /// Parses the data file as medical stats file and import as metadata to the parent experiment.
        /// The first column contains field names and the remaining columns contain the values for each data set.
        /// The first row of the file gives the name of the data set.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the operation.</param>
        /// <param name="saveChanges">Whether to update the database context or wait for SaveChanges to be called manually.</param>
        /// <param name="delim">The delimiter used in the file</param>
        /// <param name="requestedFieldNos">A list of the fields to import (with zero being the first field)</param>
        /// <param name="requestedDatasetNos">A list of the datasets to import (with zero being the first dataset)</param>
        /// <returns>A count of the number of parameters created</returns>
        public int ImportAsStatsFile(MaterialsMetadataContext dbcontext, bool saveChanges, char delim, List<int> requestedFieldNos, List<int> requestedDatasetNos)
        {
            if (!this.ExperimentReference.IsLoaded)
            {
                this.ExperimentReference.Load();
            }

            int experimentID = this.Experiment.ID;

            int paramsCreated = 0;

            ExperimentParameter parentParam = new ExperimentParameter() { Name = this.Filename };
            dbcontext.AddParameterToExperiment(experimentID, parentParam);

            //Parse the file
            FileParser_Stats parsedFile = new FileParser_Stats(this.GetFullPath(), delim);

            //Keep a track of the parent parameters
            Dictionary<int, ExperimentParameter> datasetParams = new Dictionary<int, ExperimentParameter>();

            foreach (KeyValuePair<int, string> kvp in parsedFile.DatasetNames)
            {
                //Skip the first column as the first column does not include a title
                if (requestedDatasetNos.Count == 0 || requestedDatasetNos.Contains(kvp.Key))
                {
                    //Create a new parameter under which the metadata from the dataset will be created
                    ExperimentParameter dsParam = new ExperimentParameter() { Name = kvp.Value };
                    dbcontext.AddParameterToExperiment(experimentID, dsParam);
                    parentParam.AddChildParameter(dbcontext, dsParam);

                    //Add the parameter to the dictionary for use later
                    datasetParams.Add(kvp.Key, dsParam);

                    foreach (KeyValuePair<int, string> fieldvalue in parsedFile.FieldValues[kvp.Key])
                    {
                        if (requestedFieldNos.Count == 0 || requestedFieldNos.Contains(fieldvalue.Key))
                        {
                            //Create new parameter with the metadata
                            ExperimentParameter newparam = new ExperimentParameter() { Name = parsedFile.FieldNames[fieldvalue.Key], Value = fieldvalue.Value };
                            dbcontext.AddParameterToExperiment(experimentID, newparam);
                            paramsCreated++;

                            //Save it below the dataset's parent parameter
                            dsParam.AddChildParameter(dbcontext, newparam);
                        }
                    }
                }
            }

            if (saveChanges)
            {
                dbcontext.SaveChanges();
            }

            return paramsCreated;
        }

        /// <summary>
        /// Parses the data file as a delimited data file and imports as metadata to the parent experiment.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the operation.</param>
        /// <param name="saveChanges">Whether to update the database context or wait for SaveChanges to be called manually.</param>
        /// <param name="delim">The delimiter used in the file</param>
        /// <param name="startrow">The line to start importing from</param>
        /// <param name="headerrow">The line in the file to use as the header row (if 0 or less, header row not present)</param>
        /// <param name="maxrows">The number of rows to import</param>
        /// <param name="requestedFieldNos">A list of the fields to import (with zero being the first field)</param>
        /// <returns>The number of metadata entries created</returns>
        public int ImportAsDelimitedFile(MaterialsMetadataContext dbcontext, bool saveChanges, char delim, int startrow, int headerrow, int maxrows, List<int> requestedFieldNos)
        {
            if (!this.ExperimentReference.IsLoaded)
            {
                this.ExperimentReference.Load();
            }

            int experimentID = this.Experiment.ID;

            int paramsCreated = 0;

            //Create an empty parameter to be used as the parent for all parameters we create
            ExperimentParameter parentParam = new ExperimentParameter() { Name = this.Filename };
            dbcontext.AddParameterToExperiment(experimentID, parentParam);

            //Parse the file
            FileParser_DelimitedFile parsedFile = new FileParser_DelimitedFile(this.GetFullPath(), delim, startrow, headerrow);

            //Look this up so save time (looking up properties can be slow in long loops)
            int parsedFileFieldCount = parsedFile.FieldCount;

            //Get list of keys to use now (before we enter big loop)
            List<int> approvedFieldNumbers = new List<int>();
            foreach (int fieldNo in parsedFile.FieldNames.Keys)
            {
                if (requestedFieldNos.Count == 0 || requestedFieldNos.Contains(fieldNo))
                {
                    approvedFieldNumbers.Add(fieldNo);
                }
            }

            Dictionary<int, string> fieldValues;
            while ((fieldValues = parsedFile.GetNextRowValues()) != null)
            {
                //Look this up so save time (looking up properties can be slow in long loops)
                int currentLineNo = parsedFile.CurrentLineNoRel;

                //If maxrows <= 0 then it is not valid so we ignore it.
                if ((maxrows > 0) && (currentLineNo > maxrows))
                {
                    //Break if we're done reading
                    break;
                }

                foreach (int fieldNo in approvedFieldNumbers)
                {
                    //Check our data row has the right amount of data
                    if (fieldValues.Count == parsedFileFieldCount && fieldValues.ContainsKey(fieldNo))
                    {
                        //Create new parameter with the metadata
                        ExperimentParameter newparam = new ExperimentParameter() { Name = parsedFile.FieldNames[fieldNo], Value = fieldValues[fieldNo], Position = currentLineNo * fieldNo };
                        dbcontext.AddParameterToExperiment(experimentID, newparam);
                        paramsCreated++;

                        //Save it below the dataset's parent parameter
                        parentParam.AddChildParameter(dbcontext, newparam);
                    }
                }
            }

            if (saveChanges)
            {
                dbcontext.SaveChanges();
            }

            return paramsCreated;
        }

    }
}
